import numpy as np
import json
from sklearn.metrics.pairwise import cosine_similarity
from data_preparation import DataPreparationLayer  # 导入数据准备层
from user_info_recommender import UserInfoRecommender  # 导入用户信息推荐器
import os
from collections import defaultdict
from typing import List, Dict
class RecommendationEngine:
    def __init__(self, data_layer: DataPreparationLayer):
        """初始化推荐引擎，依赖数据准备层实例"""
        self.data_layer = data_layer  # 数据准备层实例（包含模板向量和用户行为）
        self.template_vectors = self._load_all_template_vectors()  # 缓存所有模板向量
        self.template_ids = list(self.template_vectors.keys())     # 所有模板ID列表
        # 确保模板元数据同步（从ChromaDB加载所有模板信息）
        self._sync_template_metadata()
        print(f"推荐引擎初始化完成，加载模板数量：{len(self.template_ids)}")
        # 关键修复：显式初始化user_info_recommender
        try:
            self.user_info_recommender = UserInfoRecommender(
                user_vector_path="/home/ai/Recommendation_system/user_vector_data.json",
                template_vector_path="/home/ai/Recommendation_system/template_vectors.json"
            )
            print("✓ 用户信息推荐器初始化成功")
        except Exception as e:
            print(f"⚠ 用户信息推荐器初始化失败：{e}")
            self.user_info_recommender = None  # 初始化失败时设为None，后续处理
        
        # 其他属性初始化
        self.user_behavior = data_layer.user_behavior
        self.templates = data_layer.templates


    def _sync_template_metadata(self):
        """从ChromaDB同步所有模板的元数据到data_layer.templates"""
        try:
            all_data = self.data_layer.template_collection.get(include=["metadatas"])
            for tid_str, metadata in zip(all_data["ids"], all_data["metadatas"]):
                tid = int(tid_str)
                # 更新模板元数据（确保data_layer.templates包含所有模板）
                self.data_layer.templates[tid] = {
                    "name": metadata.get("name", "未知名称"),
                    "description": metadata.get("description", "无描述"),
                    "hangye": metadata.get("hangye", "未知行业")
                }
            print(f"已同步{len(all_data['ids'])}个模板的元数据")
        except Exception as e:
            print(f"同步模板元数据失败：{e}")

    def _load_all_template_vectors(self) -> dict:
        """从ChromaDB加载所有模板的向量数据"""
        try:
            # 从集合中获取所有模板的ID和向量
            all_data = self.data_layer.template_collection.get(
                include=["embeddings", "metadatas"]
            )
            
            # 构建{template_id: vector}字典
            vectors = {}
            for tid_str, embedding in zip(all_data["ids"], all_data["embeddings"]):
                vectors[int(tid_str)] = np.array(embedding)  # 转为numpy数组便于计算
            
            return vectors
        except Exception as e:
            print(f"加载模板向量失败：{e}")
            return {}



#-----------------------------------基于内容的推荐方法---------------------------------
    def _get_user_preference_vector(self, user_id: int) -> np.ndarray:
        """
            计算用户偏好向量（基于用户行为的加权平均）
            用于将用户的历史行为（如下载、收藏、浏览模板）转化为可计算的语义向量（用户偏好向量），从而量化用户的兴趣
        """
        #其中.user_behavior结构为{user_id: {template_id: score}},获取指定用户的行为数据
        user_behavior = self.data_layer.user_behavior.get(user_id, {})
        
        # 处理冷启动：用户无行为数据时返回None
        if not user_behavior:
            return None
        
        # 加权计算偏好向量（行为分数作为权重）
        total_weight = 0
        preference_vector = np.zeros(1024)  # bge-large-zh-v1.5向量维度为1024
        #初始化一个 1024 维的零向量（与模板向量维度一致，因为使用的 bge-large-zh-v1.5 模型生成的向量固定为 1024 维），用于累加用户对各模板的加权向量。
        
        for template_id, score in user_behavior.items():
            if template_id in self.template_vectors:
                preference_vector += self.template_vectors[template_id] * score
                total_weight += score
        
        # 归一化偏好向量
        if total_weight > 0:
            preference_vector /= total_weight
            return preference_vector
        return None

    def _calculate_similarity(self, target_vector: np.ndarray) -> dict:
        """计算目标向量与所有模板的余弦相似度"""
        similarities = {}
        for template_id, vector in self.template_vectors.items():
            # 计算余弦相似度（因向量已归一化，可用点积替代）
            sim = np.dot(target_vector, vector)
            similarities[template_id] = float(sim)  # 转为Python float便于处理
        
        # 按相似度降序排序
        return dict(sorted(similarities.items(), key=lambda x: x[1], reverse=True))

    def recommend_for_user(self, user_id: int, top_n: int = 3, exclude_interacted: bool = True) -> list:
        """
        为指定用户生成推荐
        :param user_id: 用户ID
        :param top_n: 推荐数量
        :param exclude_interacted: 是否排除用户已交互的模板
        :return: 推荐的模板ID列表（按推荐分数降序）
        """
        # 1. 获取用户偏好向量
        user_vector = self._get_user_preference_vector(user_id)
        
        # 2. 冷启动处理：用户无行为时返回热门模板
        if user_vector is None:
            print(f"用户{user_id}无行为数据，返回热门模板")
            return self.get_popular_templates(top_n)
        
        # 3. 计算相似度
        similarities = self._calculate_similarity(user_vector)
        
        # 4. 排除已交互的模板
        '''
        1. 避免推荐重复内容，减少信息冗余
            用户已经交互过的模板（如下载、收藏、多次浏览）通常意味着：
            用户可能已经获取了该模板，再次推荐不会带来新价值（例如用户下载过 “项目管理模板” 后，重复推荐该模板会显得冗余）。
            过度推荐已交互内容会让用户觉得系统 “没有新东西”，降低对推荐列表的关注度。通过排除已交互模板，推荐列表能优先展示用户未接触过的内容，保持推荐的新鲜感。
        2. 聚焦潜在兴趣，拓展用户体验
            推荐系统的核心目标不仅是 “重复用户喜欢的内容”，更要 “挖掘用户可能感兴趣但尚未发现的内容”。
            如果不排除已交互内容，高相似度的已交互模板可能长期占据推荐列表顶部，挤压新内容的展示机会，限制用户的探索范围。
        3. 符合用户心理预期
            多数用户使用推荐功能时，隐含需求是 “发现更多类似但没见过的内容”，而非 “重温已有的内容”。
            
        '''
        if exclude_interacted:
            interacted = self.data_layer.user_behavior.get(user_id, {}).keys()
            similarities = {tid: sim for tid, sim in similarities.items() if tid not in interacted}
        
        # 5. 返回Top N推荐
        top_templates = list(similarities.keys())[:top_n]
        return top_templates

    def get_popular_templates(self, top_n: int = 3) -> list:
        """获取热门模板（基于所有用户的交互次数）"""
        # 统计所有模板的交互次数（权重求和）
        template_scores = {}
        for user_behavior in self.data_layer.user_behavior.values():
            for tid, score in user_behavior.items():
                if tid in template_scores:
                    template_scores[tid] += score
                else:
                    template_scores[tid] = score
        
        # 按分数降序排序，返回Top N
        sorted_templates = sorted(template_scores.items(), key=lambda x: x[1], reverse=True)
        
        #---------------------------测试--------------------------
        for tid, score in sorted_templates:
            # 确保模板信息存在
            if tid not in self.data_layer.templates:
                print(f"模板ID {tid} 信息未找到，跳过")
                continue
            
            # 打印模板信息（增强健壮性）
            template_info = self.data_layer.templates[tid]
            print(f"模板ID={tid}, 名称={template_info['name']}, 行业={template_info['hangye']}, 分数={score}")


        return [tid for tid, _ in sorted_templates[:top_n]]

    def recommend_by_content(self, template_id: int, top_n: int = 3) -> list:
        """基于模板内容相似度推荐（相似模板）"""
        if not self.template_vectors:
            print("模板向量未加载，无法推荐")
            return []
        if template_id not in self.template_vectors:
            print(f"模板{template_id}不存在")
            return []
        
        # 以指定模板向量为目标，计算相似度
        target_vector = self.template_vectors[template_id]
        similarities = self._calculate_similarity(target_vector)
        
        # 排除自身，返回Top N
        similar_templates = [tid for tid in similarities.keys() if tid != template_id][:top_n]
        return similar_templates

    def explain_recommendation(self, user_id: int, template_id: int) -> str:
        """测试：生成推荐解释（为什么推荐该模板给用户）"""
        # 先获取模板信息，增强健壮性
        template_info = self.data_layer.get_template_info(template_id)
        if not template_info:
            return f"模板{template_id}信息不存在"
        
        user_behavior = self.data_layer.user_behavior.get(user_id, {})
        
        # 找到用户最相似的历史交互模板
        if user_behavior:
            user_vector = self._get_user_preference_vector(user_id)
            if user_vector is not None and template_id in self.template_vectors:
                sim = np.dot(user_vector, self.template_vectors[template_id])
                return (f"推荐「{template_info['name']}」是因为它与您之前关注的内容相似度较高（匹配度：{sim:.2f}），"
                        f"属于{template_info['hangye']}领域")
        
        # 冷启动推荐解释
        return f"推荐「{template_info['name']}」是因为它在{template_info['hangye']}领域中很受欢迎"



#   -------------------------------基于协调过滤的推荐方法---------------------------------------------
    def _calculate_user_similarity(self) -> dict:
        """计算所有用户之间的相似度（基于行为的余弦相似度）"""
        # 1. 构建用户-模板行为矩阵（用户为行，模板为列，值为行为分数）
        all_users = list(self.data_layer.user_behavior.keys())
        all_templates = self.template_ids
        user_template_matrix = []
        
        for user_id in all_users:
            # 为每个用户构建行为向量（与模板列表顺序对应）
            behavior_vector = []
            for tid in all_templates:
                behavior_vector.append(self.data_layer.user_behavior[user_id].get(tid, 0))
            user_template_matrix.append(behavior_vector)
        
        # 2. 计算用户间余弦相似度
        if len(all_users) < 2:
            return {}  # 不足2个用户时无法计算相似度
        
        user_similarity_matrix = cosine_similarity(user_template_matrix)
        
        # 3. 构建{目标用户: {相似用户: 相似度分数}}字典
        user_similarities = {}
        for i, target_user in enumerate(all_users):
            similar_users = {}
            for j, other_user in enumerate(all_users):
                if i != j:  # 排除自身
                    similar_users[other_user] = user_similarity_matrix[i][j]
            # 按相似度降序排序
            user_similarities[target_user] = dict(
                sorted(similar_users.items(), key=lambda x: x[1], reverse=True)
            )
        return user_similarities

    def recommend_collaborative(self, user_id: int, top_n: int = 3, exclude_interacted: bool = True, k: int = 3) -> list:
        """
        基于用户协同过滤的推荐：找相似用户喜欢的模板
        :param k: 参考前k个最相似用户的行为
        """
        # 1. 加载或计算用户相似度
        similarity_file = "/home/ai/Recommendation_system/user_similarities_data.json"
        user_similarities = None
        
        # 检查文件是否存在并尝试加载
        if os.path.exists(similarity_file):
            try:
                with open(similarity_file, 'r', encoding='utf-8') as f:
                    user_similarities = json.load(f)
                print(f"成功从{similarity_file}加载用户相似度数据")
            except Exception as e:
                print(f"加载用户相似度文件失败: {str(e)}, 将重新计算")
        
        # 如果加载失败或文件不存在，使用原方法计算
        if user_similarities is None:
            user_similarities = self.data_layer._calculate_user_similarity()

        # 2. 验证用户ID是否存在于相似度数据中（使用字符串类型的用户ID）
        user_id_str = str(user_id)
        if user_id_str not in user_similarities:
            print(f"警告：用户{user_id}不存在于相似度数据中")
            return []
            
        # 3. 获取前k个最相似的用户
        similar_users = list(user_similarities[user_id_str].keys())[:k]
        print(f"用户{user_id}的相似用户（前{k}）：{similar_users}")

        # 4. 收集相似用户喜欢的模板（加权分数=相似度×行为分数）
        template_scores = defaultdict(float)
        # 注意这里使用字符串类型的user_id_str访问
        for similar_user, similarity in user_similarities[user_id_str].items():
            if similar_user in similar_users:  # 只取前k个
                for tid, score in self.data_layer.user_behavior.get(similar_user, {}).items():
                    # 分数=用户相似度×行为权重（相似度越高，影响力越大）
                    template_scores[tid] += similarity * score
        
        # 5. 排除已交互的模板
        if exclude_interacted:
            interacted = self.data_layer.user_behavior.get(str(user_id), {}).keys()  # 同样转为字符串
            template_scores = {tid: s for tid, s in template_scores.items() if tid not in interacted}
        
        # 6. 按分数降序返回Top N
        sorted_templates = sorted(template_scores.items(), key=lambda x: x[1], reverse=True)
        return [tid for tid, _ in sorted_templates[:top_n]]
    
#   -------------------------------基于内容的用户信息的推荐方法---------------------------------------------


























#-------------------------------------------混合推荐方法---------------------------------------------
    def recommend_hybrid(
        self, 
        user_id: int, 
        top_n: int = 3,
        content_weight: float = 0.5,    # 基于内容的推荐占比50%
        user_info_weight: float = 0.2,  # 基于用户信息的推荐占比20%
        collab_weight: float = 0.3      # 协同过滤推荐占比30%
    ) -> List[int]:
        """
        混合推荐：结合三种推荐方法
        - 基于内容的推荐：50%
        - 基于用户信息的推荐：20%
        - 协同过滤推荐：30%
        """
        # 1. 处理冷启动：无用户数据时返回热门模板
        user_vector = self._get_user_preference_vector(user_id)
        if user_vector is None:
            print(f"用户{user_id}无行为数据，返回热门模板")
            return self.get_popular_templates(top_n)
        
        # 2. 获取三种推荐结果（多取一些用于融合）
        # 基于内容的推荐
        content_recs = self.recommend_for_user(user_id, top_n * 3)
        # 基于用户信息的推荐
        try:
            # 从用户信息推荐器获取带分数的推荐结果
            user_info_recs_with_scores = self.user_info_recommender.recommend(
                user_id=user_id,
                top_n=top_n * 3,
                exclude_ids=list(self.user_behavior.get(user_id, {}).keys())
            )
            # 提取模板ID列表
            user_info_recs = [tid for tid, _ in user_info_recs_with_scores]
        except Exception as e:
            print(f"基于用户信息的推荐失败：{e}，将降低其权重")
            user_info_recs = []
            user_info_weight *= 0.5  # 出错时降低权重
        # 协同过滤推荐
        collab_recs = self.recommend_collaborative(user_id, top_n * 3)
        
        # 3. 合并结果并计算加权分数
        hybrid_scores = defaultdict(float)
        total_items = top_n * 3  # 每种推荐的总数量
        
        # 3.1 基于内容的推荐打分
        for i, tid in enumerate(content_recs):
            hybrid_scores[tid] += (total_items - i) * content_weight
        
        # 3.2 基于用户信息的推荐打分
        for i, tid in enumerate(user_info_recs):
            hybrid_scores[tid] += (total_items - i) * user_info_weight
        
        # 3.3 协同过滤推荐打分
        for i, tid in enumerate(collab_recs):
            hybrid_scores[tid] += (total_items - i) * collab_weight
        
        # 4. 按分数排序并返回Top N
        sorted_recommendations = sorted(
            hybrid_scores.items(), 
            key=lambda x: x[1], 
            reverse=True
        )
        
        # 提取模板ID并返回
        return [tid for tid, _ in sorted_recommendations[:top_n]]



# 测试推荐引擎
if __name__ == "__main__":

    # 初始化数据层（使用已有的数据库，不重新添加模板）
    data_layer = DataPreparationLayer(
        model_path="/home/ai/huggingface_cache/BAAI/bge-large-zh-v1.5/bge-large-zh-v1.5",
        chroma_persist_dir="/home/ai/Recommendation_system/chroma_template_db",
        behavior_data_path="./user_behavior_data.json"
    )
    
    # 初始化推荐引擎
    engine = RecommendationEngine(data_layer)
    
    # 测试用户推荐（用户101有下载模板1和收藏模板3的行为）
    print("\n=== 用户103的推荐(基于内容推荐)结果 ===")
    user101_recs = engine.recommend_for_user(user_id=101, top_n=2)
    for i, tid in enumerate(user101_recs, 1):
        # 增强健壮性：先判断模板信息是否存在
        info = data_layer.get_template_info(tid)
        if info:
            print(f"{i}. {info['name']}（{info['hangye']}）")
            print(f"   推荐理由：{engine.explain_recommendation(101, tid)}")
        else:
            print(f"{i}. 模板ID={tid}（信息未找到）")
    
    # 测试冷启动推荐（用户200无任何行为）
    print("\n=== 新用户200的推荐结果 ===")
    new_user_recs = engine.recommend_hybrid(user_id=200, top_n=2)
    for i, tid in enumerate(new_user_recs, 1):
        info = data_layer.get_template_info(tid)
        if info:
            print(f"{i}. {info['name']}（{info['hangye']}）")
        else:
            print(f"{i}. 模板ID={tid}（信息未找到）")
    
    #测试基于协调过滤的推荐
    print("\n=== 用户103的推荐(基于协同过滤推荐)结果 ===")
    user103_recs = engine.recommend_collaborative(user_id=103, top_n=2)
    for i, tid in enumerate(user103_recs, 1):
        info = data_layer.get_template_info(tid)
        if info:
            print(f"{i}. {info['name']}（{info['hangye']}）")
        else:
            print(f"{i}. 模板ID={tid}（信息未找到）")
    
    # 测试混合推荐（基于用户推荐）
    print("\n===基于（混合方法推荐）的结果 ===")
    similar_recs = engine.recommend_hybrid(user_id=103, top_n=2)
    for i, tid in enumerate(similar_recs, 1):
        info = data_layer.get_template_info(tid)
        if info:
            print(f"{i}. {info['name']}")
        else:
            print(f"{i}. 模板ID={tid}（信息未找到）")